Skip to content

react 如何实现 requestIdleCallback

什么是 requestIdleCallback

requestIdleCallback 是浏览器提供的 API,可以让开发者在浏览器空闲时执行一些任务。

如何使用 requestIdleCallback

javascript
function requestIdleCallbackExample() {
  if (window.requestIdleCallback) {
    window.requestIdleCallback(function(time) {
      if (time.remaining > 0) {
        // do something
      }
    });
  } else {
    setTimeout(function() {
      // do something
    }, 0);
  }
}

React 为什么要自己实现 requestIdleCallback

  • requestIdleCallback 兼容性问题
  • 为了更好的性能,浏览器 requestIdleCallback 是 50ms 执行一次,也就是 20 帧每秒,如果 React 使用这种渲染机制,那么会导致页面卡顿。

基于以上原因,React 自己实现了一个 requestIdleCallback,我们来看 Web 端的实现思路。

实现思路

  • 我们要知道每一帧的时间间隔,也就是屏幕一帧需要的时间 假设是 16.6ms
  • 从执行当前线程开始,屏幕更新 完成时间大概是 16.6ms 后
  • 屏幕更新可能不需要 16.6ms,我们需要在屏幕更新后获取当前时间,用于判断是否有剩余时间
js
// 自执行函数
(function () {
  let frameEndTime;
  let penddingCallback;
  // 时间切片的时间间隔 (react 默认是 5ms,也对外提供了修改的函数,我们可以根据设备的刷新率做取舍)
  let yieldInterval = 16.6 // ms
  // 获取此帧绘制完成的时机,MessageChannel

  const channel = new MessageChannel();

  requestIdleCallback = function (callback) {
    penddingCallback = callback;
    // rafTime requestAnimationFrame 回调执行的时间,说明本帧开始绘制
    requestAnimationFrame((rafTime) => {
      // 假设 需要 16.6ms 绘制完这一帧,那么预计绘制完成的时间是
      frameEndTime = rafTime + yieldInterval;
      // 随便发消息,以便在动画帧结束能获取消息
      channel.port1.postMessage("raf cb execute");
    });
  };

  channel.port2.onmessage = () => {
    // requestAnimation 执行结束时机
    // 当前帧可用时间
    const time = timeRemaining();
    // 如果有剩余时间
    if (time > 0) {
      penddingCallback &&
        penddingCallback({
          timeRemaining,
          // 肯定没有超时
          didTimeout: false,
        });
    } else {
      // 简单粗暴处理 转移到下一帧处理
      requestIdleCallback(penddingCallback);
    }
  };

  function timeRemaining() {
    return frameEndTime - performance.now();
  }
})();

// 测试代码
requestIdleCallback((time) => {
  console.log('剩余时间',time.timeRemaining(),'ms');
});

上面的 16.6 ms,对于不同设备是不一样的,刷新率比价高的设备上,会小于 16.6,会造成当前帧实际没有空闲时间,但是代码判断有空闲时间,会占用主任务时间。

我们来计算一下设备的 fps,简单思路:

js
/**
 * @param targetCount 采样次数
 * @param recalc 是否重新计算
 */
function getFPS(targetCount = 60, recalc) {
  // 我们挂载到 __FPS__
  // 
  if (window.__FPS__&&!recalc) return Promise.resolve(window.__FPS__)
  const startTime = performance.now();
  let count = targetCount;
  return new Promise((resolve) => {
    (function fps() {
      requestAnimationFrame(() => {
        count--;
        if (count === 0) {
          resolve(targetCount / ((performance.now() - startTime) / 1000));
          return;
        }
        fps();
      });
    })();
  });
}
getFPS().then(fps => {
  console.log(fps, '帧率')
  window.__FPS__ = fps
});

如果我们想处理精确的帧率,应该是算出每一帧的时间 也就是 1000 / window.__FPS__

但是React 是默认 5ms 的时间切片,如果这个时间不够,就会放弃当前任务去做高优先级的任务。

总结

requestIdleCallback 是 React 脱离平台的实现方案,不仅解决了浏览器的兼容问题,也能更精准的做到时间分片的控制,